#!/usr/bin/env python
#
# Scanner implementation for SCF2
#
# Author:   Mike Murphy (mamurph@cs.clemson.edu)
# Revision: 14 July 2008
#
#   Copyright 2007-2008 Clemson University
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# This material is based upon work supported under a National Science
# Foundation Graduate Research Fellowship. Any opinions, findings,
# conclusions or recommendations expressed in this publication are those of
# the author(s) and do not necessarily reflect the views of the National
# Science Foundation.

import sys, os, os.path
from messages import MSG_SCAN_ERROR, MSG_SCAN_ERROR_WITH_LINE, \
      MSG_SYNTAX_ERROR, MSG_SCAN_WARNING, MSG_SCAN_WARNING_WITH_LINE, \
      MSG_CIRCULAR_IMPORT, MSG_IMPORT_SCAN_FAIL

# Scanner format: (type, state, line, string, alt, filename)

SCT_TOKEN = 'token'
SCT_CONTROL = 'control'
SCT_STRING = 'string'
SCT_BOOLSTRING = 'boolstring'

S_LHS = 100
S_RHS = 101
S_CONTINUE = 102
S_ESCAPE = 103
S_ENV = 104
S_WAIT_FOR_CONTINUE = 105

ESCAPE_TABLE = {  'n' : '\n',
                  't' : '\t',
                  'b' : '\b',
                  'a' : '\a',
               }

def scan_warning(filename, message, line = -1):
   msg = MSG_SCAN_WARNING % (filename, message)
   if line >= 0:
      msg = MSG_SCAN_WARNING_WITH_LINE % (filename, line, message)
   print >> sys.stderr, msg

def scan_error(filename, message, line = -1):
   msg = MSG_SCAN_ERROR % (filename, message)
   if line >= 0:
      msg = MSG_SCAN_ERROR_WITH_LINE % (filename, line, message)
   print >> sys.stderr, msg

def read_file(filename):
   data = ''
   ok = True
   try:
      fh = open(filename, 'r')
      data = fh.read()
      fh.close()
   except IOError, message:
      scan_error(filename, str(message))
      ok = False
   return (ok, data)

class CfgScanner(object):
   def __init__(self, filepath, input_text, blacklist = []):
      #
      self.filepath = filepath
      if not os.path.isabs(self.filepath):
         abspath = os.path.abspath(self.filepath)
         self.filepath = abspath
      self.base_dir = os.path.dirname(self.filepath)
      self.filename = os.path.split(self.filepath)[1]
      self.blacklist = blacklist[:]
      self.blacklist.append(self.filepath)
      self.state_table = { S_LHS : self.scan_lhs,
                           S_RHS : self.scan_rhs,
                           S_CONTINUE : self.scan_continue,
                           S_ESCAPE : self.scan_escape,
                           S_ENV : self.scan_env,
                           S_WAIT_FOR_CONTINUE : self.scan_wfc,
                         }
      #
      self.scan_results = []
      self.buffer = input_text
      self.index = 0
      self.line = 1
      self.mark_point = 0
      self.mark_line = 1
      self.ok = True
      self.state = S_LHS
      self.cur_tok = ''
      #
      self.last_state = S_LHS
   def do_import(self, filename):
      if not os.path.isabs(filename):
         filename = os.path.normpath(os.path.join(self.base_dir, filename))
      if filename in self.blacklist:
         scan_warning(self.filename, \
               MSG_CIRCULAR_IMPORT % (filename), self.line)
      else:
         ok, data = read_file(filename)
         if ok:
            subscanner = CfgScanner(filename, data, self.blacklist)
            s_ok, results = subscanner.scan()
            self.scan_results.extend(results)
            if not s_ok:
               scan_error(self.filename, MSG_IMPORT_SCAN_FAIL % (filename), \
                  self.line)
               self.ok = False
         else:
            self.ok = False
   def read_char(self):
      ch = ''
      if self.index < len(self.buffer):
         ch = self.buffer[self.index]
         self.index += 1
         if ch == '\n':
            self.line += 1
      #print 'read:', ch, 'state:', self.state
      return ch
   def strip_comment(self):
      idx = self.index
      while idx < len(self.buffer) and self.buffer[idx] != '\n':
         idx += 1
      self.index = idx # leave the newline
   def mark(self):
      self.mark_point = self.index
      self.mark_line = self.line
   def scan(self):
      while self.index < len(self.buffer):
         self.state_table[self.state]()
      # Flush state machine
      self.state_table[self.state]()
      return (self.ok, self.scan_results)
   def accept(self, scantype):
      alt = self.cur_tok
      lit = self.cur_tok.strip()
      line = self.mark_line
      acttype = scantype
      if scantype == SCT_BOOLSTRING:
         if lit.lower() == 'true':
            lit = True
         elif lit.lower() == 'false':
            lit = False
         else:
            acttype = SCT_STRING
      self.cur_tok = ''
      self.mark()
      self.scan_results.append(\
         (acttype, self.state, line, lit, alt, self.filename))
      # Bug warning
      if self.state not in (S_RHS, S_LHS):
         print >> sys.stderr, 'BUG: scf2 scanner: accepted in state', self.state
   def scan_wfc(self):
      ch = self.read_char()
      if ch == '#':
         self.strip_comment()
      elif ch == '\n':
         self.state = S_CONTINUE
      elif ch.isspace():
         pass # consume whitespace
      else:
         scan_error(self.filename, MSG_SYNTAX_ERROR, self.line)
   def scan_escape(self):
      ch = self.read_char()
      if ch in ESCAPE_TABLE:
         self.cur_tok += ESCAPE_TABLE[ch]
         self.state = self.last_state
      elif ch == '\n':
         self.state = S_CONTINUE
      elif ch.isspace():
         self.state = S_WAIT_FOR_CONTINUE
      elif ch == '':
         scan_error(self.filename, MSG_SYNTAX_ERROR, self.line)
         self.ok = False
      else:
         self.cur_tok = self.cur_tok + ch
         self.state = self.last_state
   def scan_lhs(self):
      ch = self.read_char()
      if ch == '=':
         lhs = self.cur_tok.strip()
         self.cur_tok = lhs
         self.accept(SCT_STRING)
         self.cur_tok = '='
         self.accept(SCT_TOKEN)
         self.state = S_RHS
      elif ch == '\n' or ch == '':
         lhs = self.cur_tok.strip()
         if lhs.startswith('import '):
            # do the import
            filename = lhs[7:].strip()
            self.do_import(filename)
            # Stay in LHS state, but clear token and mark
            self.cur_tok = ''
            self.mark()
         elif lhs.startswith('unset '):
            # defer for parser
            self.cur_tok = 'unset'
            self.accept(SCT_CONTROL)
            self.cur_tok = lhs[6:].strip()
            self.accept(SCT_STRING)
            # Stay in LHS state, but clear token and mark
            self.cur_tok = ''
            self.mark()
         elif lhs == '':
            self.cur_tok = ''
            # Stay in LHS state, but mark
            self.mark()
         else:
            sct = SCT_STRING
            data = self.cur_tok.strip()
            if data in ('[', ']', '{', '}'):
               sct = SCT_TOKEN
            self.accept(sct)
      elif ch == '#':
         self.strip_comment()
         tok = self.cur_tok.strip()
         if tok != '':
            self.accept(SCT_BOOLSTRING)
         else:
            self.cur_tok = ''
            # Stay in LHS state
      elif ch == '\\':
         self.last_state = S_LHS
         self.state = S_ESCAPE
      elif ch == '$':
         self.last_state = S_LHS
         self.state = S_ENV
      elif ch == '[' and (self.cur_tok == '' or self.cur_tok.isspace()):
         self.cur_tok = '['
         self.accept(SCT_TOKEN)
         # Stay in LHS state
      else:
         self.cur_tok += ch
         # Still LHS
   def scan_rhs(self):
      ch = self.read_char()
      if ch == '\n' or ch == '':
         rhs = self.cur_tok.strip()
         self.accept(SCT_BOOLSTRING)
         self.state = S_LHS
      elif ch == '\\':
         self.last_state = S_RHS
         self.state = S_ESCAPE
      elif ch == '$':
         self.cur_tok += '$'
         self.last_state = S_RHS
         self.state = S_ENV
      elif ch == '#':
         self.strip_comment()
      elif ch == '{' and (self.cur_tok == '' or self.cur_tok.isspace()):
         self.cur_tok = ch
         self.accept(SCT_TOKEN)
         self.state = S_LHS
      elif ch == '[' and (self.cur_tok == '' or self.cur_tok.isspace()):
         self.cur_tok = ch
         self.accept(SCT_TOKEN)
         self.state = S_LHS
      else:
         self.cur_tok += ch
   def scan_continue(self):
      ch = self.read_char()
      #if ch == '#' or ch == '\n':
      #   # Comments have already been stripped, as has newline
      #   scan_error(self.filename, MSG_SYNTAX_ERROR, self.line)
      if ch == '#':
         self.strip_comment()
         # Can't be a continue situation, so return to saved state
         self.state = self.last_state
      elif ch.isspace():
         pass # strip whitespace: if error, let parser catch it
      elif ch == '$':
         # Possible environment variable begins continued line
         self.cur_tok += '$'
         self.state = S_ENV
         # last_state is still whatever it was before the continue
      elif ch == '\\':
         # Possible escape begins continued line
         self.state = S_ESCAPE
      elif ch == '\n':
         # Continuation char led to blank line, or line with only comment
         self.state = self.last_state
      else:
         self.cur_tok += ch
         self.state = self.last_state # Restore previous state on new line
   def scan_env(self):
      ch = self.read_char()
      if ch == '$':
         if self.cur_tok.endswith('$'):
            # This was an escape sequence for a literal $
            self.state = self.last_state
         else:
            # Find the previous $:
            start = self.cur_tok.rfind('$')
            envname = self.cur_tok[start+1:]
            envval = os.getenv(envname)
            if envval == None:
               envval = ''
            # do the substitution
            self.cur_tok = self.cur_tok[0:start] + envval
            self.state = self.last_state
      elif ch == '' or ch.isspace():
         scan_error(self.filename, MSG_SYNTAX_ERROR, self.line)
         self.ok = False
      else:
         self.cur_tok += ch

if __name__ == '__main__':
   toss, raw = read_file(sys.argv[1])
   scanner = CfgScanner(sys.argv[1], raw)
   ok, results = scanner.scan()
   if ok:
      for result in results:
         print result
